/*
 * Copyright 2012 Finbudget.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package finbudget.client.general;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import finbudget.client.lists.GetListOfItems.BudgetListItem;
import finbudget.client.lists.GetListOfItems.ListType;
import finbudget.client.lists.ListArrivedEvent;
import finbudget.client.lists.ListArrivedHandler;
import finbudget.client.lists.ListsManager;

/**
 *         This Oracle is intended to operate with lists of elements type BudgetListItem.
 *         The list can contain items with the same names. In this case items are
 *         shown with their's keys.
 *         To use this oracle a caller should add all items by addNoSubmit method
 *         then call submit() method to pass items to MultiWordSuggestOracle.
 *         To get BudgetListItem item by the shown name a caller should use
 *         getItemByValue() method.
 */
public class NodeSuggestionOracle extends MultiWordSuggestOracle implements ListArrivedHandler {
	public static final int itemsToShow = 1000;
	/**
	 * Maps displayed names to BudgetList item.
	 */
	private Map<String, BudgetListItem> map = new HashMap<String, BudgetListItem>();
	/**
	 * A set of names from the list of BudgetList items.
	 */
	private Set<String> names = new HashSet<String>();
	private ListType listType;
	private final ListsManager listsManager;

	public interface NodeSuggestionOracleFactory {
		public NodeSuggestionOracle create(ListType listType);
	}

	/**
	 * Default constructor.
	 */
	@Inject
	protected NodeSuggestionOracle(ListsManager listsManager, @Assisted ListType listType) {
		super("\"№(-");
		this.listType = listType == null ? null : listType.clone();
		this.listsManager = listsManager;

		listsManager.addListArrivedEventHandler(this);
		listsManager.requestList(listType);
	}

	public void put(Collection<BudgetListItem> items) {
		clear();
		// Fill the oracle in two stages according to it's logic:
		// 1) put items; 2) submit items.
		for (BudgetListItem item : items) {
			addNoSubmit(item);
		}
		submit();
	}

	public void setItemsAsDefaultSuggestions() {
		List<Suggestion> suggestionList = new ArrayList<Suggestion>();
		for (Map.Entry<String, BudgetListItem> entry : map.entrySet()) {
			suggestionList.add(new NodeSuggestion(entry.getValue().name, entry.getValue().name, entry.getValue()));
		}
		Collections.sort(suggestionList, new Comparator<Suggestion>() {

			@Override
			public int compare(Suggestion o1, Suggestion o2) {
				return o1.getDisplayString().compareTo(o2.getDisplayString());
			}

		});
		setDefaultSuggestions(suggestionList.size() <= itemsToShow ? suggestionList : suggestionList.subList(0, itemsToShow - 1));
	}

	public void setCompleteListAsDefaultSuggestions() {
		List<Suggestion> suggestionList = new ArrayList<Suggestion>();
		List<BudgetListItem> items = listsManager.getList(listType);
		if (items != null) {
			for (BudgetListItem item : items) {
				suggestionList.add(new NodeSuggestion(item.name, item.name, item));
			}
		}
		Collections.sort(suggestionList, new Comparator<Suggestion>() {

			@Override
			public int compare(Suggestion o1, Suggestion o2) {
				return o1.getDisplayString().compareTo(o2.getDisplayString());
			}

		});
		setDefaultSuggestions(suggestionList.size() <= itemsToShow ? suggestionList : suggestionList.subList(0, itemsToShow - 1));
	}

	/**
	 * Add an item to the suggestion list and do not pass it
	 * to MultiWordOracle. This method controls that all the
	 * items to be put in the list. MultiWordOracle keeps
	 * only unique items, so in case the item has not a unique
	 * name all the item with the same names are accompanied
	 * with theirs unique keys.
	 * 
	 * @param item
	 */
	public void addNoSubmit(BudgetListItem item) {
		// Add item to the set of item names and check
		// if the item's name is unique in the list.
		if (names.add(item.name)) {
			// There was no such name is the list.
			map.put(item.name, item);
		} else {
			// There is at least one such name in the set
			BudgetListItem existingItem = map.get(item.name);
			if (existingItem != null) {
				// Replace the item with the name including it's key
				map.remove(existingItem.name);
				map.put(existingItem.name + " <" + existingItem.key + ">", existingItem);
			}
			map.put(item.name + " <" + item.key + ">", item);
		}
	}

	/**
	 * Pass prepared list to MultiWordSuggestOracle.
	 */
	public void submit() {
		super.clear();
		for (Entry<String, BudgetListItem> item : map.entrySet()) {
			this.add(item.getKey());
		}
	}

	@Override
	protected MultiWordSuggestion createSuggestion(String replacementString, String displayString) {
		BudgetListItem item = map.get(replacementString);
		if (item == null)
			return null;
		return new NodeSuggestion(replacementString, displayString, item);
	}

	public BudgetListItem getItemByValue(String value) {
		return value == null ? null : map.get(value);
	}

	/**
	 * This method finds an item with the given key and returns
	 * it's display string. In case the item is not found the
	 * method creates new item with name 'unknown' and a given key.
	 * In case the map already contains an item with 'unknown' name
	 * this item is replaced by the new one.
	 * 
	 * @param key
	 * @return display name of an item.
	 */
	public String getItemNameByKey(String key) {
		if (key == null) {
			return null;
		}
		for (BudgetListItem item : map.values()) {
			if (item.key != null && key.equals(item.key)) {
				return item.name;
			}
		}
		// no such item
		return key;
	}

	public BudgetListItem getItemByKey(String key) {
		if (key != null) {
			for (BudgetListItem item : map.values()) {
				if (key.equals(item.key)) {
					return item;
				}
			}
		}
		return null;
	}

	@Override
	public void clear() {
		super.clear();
		map.clear();
		names.clear();
	}

	@Override
	public void onListArrived(ListArrivedEvent event) {
		if (event != null && listType.equals(event.getListType())) {
			this.put(event.getList());
		}
	}

	public ListType getListType() {
		return listType == null ? null : listType.clone();
	}

	/**
	 * Dynamic controls might want to change properties of the list.
	 * @return
	 */
	public void changeListType(ListType type) {
		clear();
		setItemsAsDefaultSuggestions();
		this.listType = type.clone();
		listsManager.requestList(this.listType);
	}

	public boolean isListOnClient() {
		return listsManager.isListOnClient(listType);
	}

	public int getListItemsQty() {
		return listsManager.getNumberOfItemsInList(listType);
	}

	public ListsManager getListsManager() {
		return listsManager;
	}
}
